/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * SerializedClassifier.java
 * Copyright (C) 2007-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.classifiers.misc;

import java.io.File;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Vector;

import weka.classifiers.AbstractClassifier;
import weka.classifiers.Classifier;
import weka.core.Capabilities;
import weka.core.Capabilities.Capability;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.SerializationHelper;
import weka.core.Utils;

/**
 * <!-- globalinfo-start --> A wrapper around a serialized classifier model.
 * This classifier loads a serialized models and uses it to make
 * predictions.<br/>
 * <br/>
 * Warning: since the serialized model doesn't get changed, cross-validation
 * cannot bet used with this classifier.
 * <p/>
 * <!-- globalinfo-end -->
 * 
 * <!-- options-start --> Valid options are:
 * <p/>
 * 
 * <pre>
 * -D
 *  If set, classifier is run in debug mode and
 *  may output additional info to the console
 * </pre>
 * 
 * <pre>
 * -model &lt;filename&gt;
 *  The file containing the serialized model.
 *  (required)
 * </pre>
 * 
 * <!-- options-end -->
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class SerializedClassifier extends AbstractClassifier {

    /** for serialization */
    private static final long serialVersionUID = 4599593909947628642L;

    /** the serialized classifier model used for making predictions */
    protected transient Classifier m_Model = null;

    /** the file where the serialized model is stored */
    protected File m_ModelFile = new File(System.getProperty("user.dir"));

    /**
     * Returns a string describing classifier
     * 
     * @return a description suitable for displaying in the explorer/experimenter
     *         gui
     */
    public String globalInfo() {
        return "A wrapper around a serialized classifier model. This classifier loads " + "a serialized models and uses it to make predictions.\n\n" + "Warning: since the serialized model doesn't get changed, cross-validation " + "cannot bet used with this classifier.";
    }

    /**
     * Gets an enumeration describing the available options.
     * 
     * @return an enumeration of all the available options.
     */
    @Override
    public Enumeration<Option> listOptions() {

        Vector<Option> result = new Vector<Option>();

        result.addElement(new Option("\tThe file containing the serialized model.\n" + "\t(required)", "model", 1, "-model <filename>"));

        result.addAll(Collections.list(super.listOptions()));

        return result.elements();
    }

    /**
     * returns the options of the current setup
     * 
     * @return the current options
     */
    @Override
    public String[] getOptions() {

        Vector<String> result = new Vector<String>();

        result.add("-model");
        result.add("" + getModelFile());

        Collections.addAll(result, super.getOptions());

        return result.toArray(new String[result.size()]);
    }

    /**
     * Parses the options for this object.
     * <p/>
     * 
     * <!-- options-start --> Valid options are:
     * <p/>
     * 
     * <pre>
     * -D
     *  If set, classifier is run in debug mode and
     *  may output additional info to the console
     * </pre>
     * 
     * <pre>
     * -model &lt;filename&gt;
     *  The file containing the serialized model.
     *  (required)
     * </pre>
     * 
     * <!-- options-end -->
     * 
     * @param options the options to use
     * @throws Exception if setting of options fails
     */
    @Override
    public void setOptions(String[] options) throws Exception {
        String tmpStr;

        super.setOptions(options);

        tmpStr = Utils.getOption("model", options);
        if (tmpStr.length() != 0) {
            setModelFile(new File(tmpStr));
        } else {
            setModelFile(new File(System.getProperty("user.dir")));
        }
    }

    /**
     * Returns the tip text for this property
     * 
     * @return tip text for this property suitable for displaying in the
     *         explorer/experimenter gui
     */
    public String modelFileTipText() {
        return "The serialized classifier model to use for predictions.";
    }

    /**
     * Gets the file containing the serialized model.
     * 
     * @return the file.
     */
    public File getModelFile() {
        return m_ModelFile;
    }

    /**
     * Sets the file containing the serialized model.
     * 
     * @param value the file.
     */
    public void setModelFile(File value) {
        m_ModelFile = value;

        if (value.exists() && value.isFile()) {
            try {
                initModel();
            } catch (Exception e) {
                throw new IllegalArgumentException("Cannot load model from file '" + value + "': " + e);
            }
        }
    }

    /**
     * Sets the fully built model to use, if one doesn't want to load a model from a
     * file or already deserialized a model from somewhere else.
     * 
     * @param value the built model
     * @see #getCurrentModel()
     */
    public void setModel(Classifier value) {
        m_Model = value;
    }

    /**
     * Gets the currently loaded model (can be null). Call buildClassifier method to
     * load model from file.
     * 
     * @return the current model
     * @see #setModel(Classifier)
     */
    public Classifier getCurrentModel() {
        return m_Model;
    }

    /**
     * loads the serialized model if necessary, throws an Exception if the
     * derserialization fails.
     * 
     * @throws Exception if deserialization fails
     */
    protected void initModel() throws Exception {
        if (m_Model == null) {
            m_Model = (Classifier) SerializationHelper.read(m_ModelFile.getAbsolutePath());
        }
    }

    /**
     * Returns default capabilities of the base classifier.
     * 
     * @return the capabilities of the base classifier
     */
    @Override
    public Capabilities getCapabilities() {
        Capabilities result;

        // init model if necessary
        if (m_ModelFile != null && m_ModelFile.exists() && m_ModelFile.isFile()) {
            try {
                initModel();
            } catch (Exception e) {
                System.err.println(e);
            }
        }

        if (m_Model != null) {
            result = m_Model.getCapabilities();
        } else {
            result = new Capabilities(this);
            result.disableAll();
        }

        // set dependencies
        for (Capability cap : Capability.values()) {
            result.enableDependency(cap);
        }

        result.setOwner(this);

        return result;
    }

    /**
     * Calculates the class membership probabilities for the given test instance.
     * 
     * @param instance the instance to be classified
     * @return preedicted class probability distribution
     * @throws Exception if distribution can't be computed successfully
     */
    @Override
    public double[] distributionForInstance(Instance instance) throws Exception {
        double[] result;

        // init model if necessary
        initModel();

        result = m_Model.distributionForInstance(instance);

        return result;
    }

    /**
     * loads only the serialized classifier
     * 
     * @param data the training instances
     * @throws Exception if something goes wrong
     */
    @Override
    public void buildClassifier(Instances data) throws Exception {
        // init model if necessary
        initModel();

        // can classifier handle the data?
        getCapabilities().testWithFail(data);
    }

    /**
     * Returns a string representation of the classifier
     * 
     * @return the string representation of the classifier
     */
    @Override
    public String toString() {
        StringBuffer result;

        if (m_Model == null) {
            result = new StringBuffer("No model loaded yet.");
        } else {
            result = new StringBuffer();
            result.append("SerializedClassifier\n");
            result.append("====================\n\n");
            result.append("File: " + getModelFile() + "\n\n");
            result.append(m_Model.toString());
        }

        return result.toString();
    }

    /**
     * Runs the classifier with the given options
     * 
     * @param args the commandline options
     */
    public static void main(String[] args) {
        runClassifier(new SerializedClassifier(), args);
    }
}
